package cn.kevinkun.CompCreator;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.Options;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.UsesAssets;
import com.google.appinventor.components.annotations.UsesLibraries;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.runtime.AndroidNonvisibleComponent;
import com.google.appinventor.components.runtime.AndroidViewComponent;
import com.google.appinventor.components.runtime.Canvas;
import com.google.appinventor.components.runtime.Component;
import com.google.appinventor.components.runtime.ComponentContainer;
import com.google.appinventor.components.runtime.Form;
import com.google.appinventor.components.runtime.Map;
import com.google.appinventor.components.runtime.ReplForm;
import com.google.appinventor.components.runtime.Sprite;
import com.google.appinventor.components.runtime.errors.YailRuntimeError;
import com.google.appinventor.components.runtime.util.YailList;

import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import cn.kevinkun.CompCreator.helpers.Property;
import cn.kevinkun.CompCreator.helpers.VisibleComponent;
import gnu.lists.LList;
import gnu.mapping.ProcedureN;
import gnu.mapping.SimpleSymbol;
import gnu.mapping.Symbol;
import kawa.standard.Scheme;

@DesignerComponent(version = 9, versionName = "8.9", description = "Create new component dynamicly, as of 2025/2/21", iconName = "icon.png", helpUrl = "https://kevinkun.cn/compcreator")

public class CompCreator extends AndroidNonvisibleComponent implements Component {
	Context context;
	String TAG = "CompCreator";
	Form form;
	HashMap<Object, HashMap<String, Object>> DICT = new HashMap();
	HashMap<String, ArrayList<Object>> ALLCOMPONENTS = new HashMap();
	HashMap<Object, ArrayList<Object>> CHILDREN = new HashMap();
	HashMap<Object, Object> PARENT = new HashMap();

	public CompCreator(ComponentContainer container) {
		super(container.$form());
		this.form = container.$form();

		this.context = container.$context();
		initParentChildrenMap(this.form);
	}

	@SimpleFunction(description = "Create component in a container")
	public Object Create(@Options(VisibleComponent.class) String component, Object in) {
		if (!(in instanceof ComponentContainer))
			throw new YailRuntimeError("Container needed", this.TAG);
		try {
			String classname = component;
			classname = "com.google.appinventor.components.runtime." + classname;

			Class clazz = Class.forName(classname);

			Constructor localConstructor = clazz.getConstructor(new Class[] { ComponentContainer.class });
			Object object = localConstructor.newInstance(new Object[] { in });

			this.PARENT.put(object, in);
			ArrayList chilren = (ArrayList) this.CHILDREN.get(in);
			if (chilren == null) {
				chilren = new ArrayList();
			}
			if (!chilren.contains(object)) {
				chilren.add(object);
			}
			this.CHILDREN.put(in, chilren);

			ArrayList components = (ArrayList) this.ALLCOMPONENTS.get(component);
			if (components == null) {
				components = new ArrayList();
			}
			if (!components.contains(object)) {
				components.add(object);
			}
			this.ALLCOMPONENTS.put(component, components);

			this.CHILDREN.put((Component) object, new ArrayList());

			return object;
		} catch (Exception localException) {
			throw new YailRuntimeError(localException.getMessage(), this.TAG);
		}
	}

	@SimpleFunction(description = "Remove component (and its children if any)")
	public void Remove(Object component) {
		ArrayList compToRemove = new ArrayList();
		compToRemove.add(component);
		int i = 0;
		while (i < compToRemove.size()) {
			ArrayList children = (ArrayList) this.CHILDREN.get(compToRemove.get(i));
			Iterator localIterator;
			if ((children != null) && (children.size() > 0)) {
				for (localIterator = children.iterator(); localIterator.hasNext();) {
					Object child = localIterator.next();
					compToRemove.add(child);
				}
			}
			i++;
		}

		for (int j = compToRemove.size() - 1; j >= 0; j--) {
			Object child = compToRemove.get(j);

			if ((child instanceof Sprite)) {
				try {
					Object parent = this.PARENT.get(child);
					Method method = parent.getClass().getDeclaredMethod("removeSprite", new Class[] { Sprite.class });
					method.setAccessible(true);
					method.invoke(parent, new Object[] { child });
					((Canvas) parent).getView().invalidate();
				} catch (Exception e) {
					throw new RuntimeException(e.getMessage());
				}
			} else {
				View view = ((AndroidViewComponent) child).getView();
				((ViewGroup) view.getParent()).removeView(view);
			}

			Object p = this.PARENT.get(child);
			ArrayList siblings = (ArrayList) this.CHILDREN.get(p);
			siblings.remove(child);
			this.CHILDREN.put(p, siblings);

			this.PARENT.remove(child);
			this.CHILDREN.remove(child);
			this.DICT.remove(child);
		}
	}

	@SimpleFunction(description = "get Children list of a container component.If component is not container, or do not exist, this will return empty list. \n======\nDo not use this to get children of Screen in companion\nNot working with Map.\n======")
	public YailList Children(Object component) {
		if (this.CHILDREN.containsKey(component)) {
			return YailList.makeList((List) this.CHILDREN.get(component));
		}
		return YailList.makeEmptyList();
	}

	@SimpleFunction(description = "get component list with same type in the container. \n======\\nDo not use Screen as container in companion\n====== ")
	public YailList ListComponents(@Options(VisibleComponent.class) String component, Object in, boolean recursive) {
		ArrayList list = new ArrayList();
		ArrayList result = new ArrayList();
		int i;
		if (recursive) {
			list.add(in);
			i = 0;
			while (i < list.size()) {
				ArrayList children = (ArrayList) this.CHILDREN.get(list.get(i));
				Iterator localIterator;
				if ((children != null) && (children.size() > 0)) {
					for (localIterator = children.iterator(); localIterator.hasNext();) {
						Object child = localIterator.next();
						list.add(child);
					}
				}
				i++;
			}
			if (result.contains(in))
				result.remove(in);
		} else {
			ArrayList children = (ArrayList) this.CHILDREN.get(in);
			if ((children != null) && (children.size() > 0)) {
				for (Iterator it = children.iterator(); it.hasNext();) {
					Object child = it.next();
					list.add(child);
				}
			}
		}

		for (int j = 0; j < list.size(); j++) {
			Object child = list.get(j);
			if (child.getClass().getSimpleName().equals(component)) {
				result.add(child);
			}
		}

		return YailList.makeList(result);
	}

	@SimpleFunction(description = "get Parent of a component. If component is screen, or not exists, this will return empty string")
	public Object Parent(Object component) {
		if (this.PARENT.containsKey(component)) {
			return this.PARENT.get(component);
		}
		return "";
	}

	public Method getMethod(Method[] paramArrayOfMethod, String paramString, int paramInt) {
		paramString = paramString.replaceAll("[^a-zA-Z0-9]", "");
		for (Method localMethod : paramArrayOfMethod) {
			int k = localMethod.getParameterTypes().length;
			if ((localMethod.getName().equals(paramString)) && (k == paramInt)) {
				return localMethod;
			}
		}
		return null;
	}

	@SimpleFunction(description = "Set a property of the specified component. 1.if property is same to one of the in-build perperty, it will set this perperty. 2.if property is 'index', it will set the order of the component.3. if any other string, it will attach a custom property on this component")
	public void SetValue(Component component, @Options(Property.class) String property, Object value) {
		if (this.PARENT.containsKey(component)) {
			if (property.toLowerCase().equals("index")) {
				SetIndex((AndroidViewComponent) component, Integer.parseInt(value.toString()));
				return;
			}
			Method[] arrayOfMethod = component.getClass().getMethods();
			Object[] arrayOfObject = YailList.makeList(new Object[] { value }).toArray();
			Method localMethod = getMethod(arrayOfMethod, property, arrayOfObject.length);
			if (localMethod == null) {
				HashMap prop = (HashMap) this.DICT.get(component);
				if (prop == null) {
					prop = new HashMap();
				}
				prop.put(property, value);
				this.DICT.put(component, prop);
			} else {
				if (property.indexOf("Color") > -1) {
					value = Integer.valueOf(parseColor(value.toString()));
				}
				Invoke(component, property, YailList.makeList(new Object[] { value }));
			}
		} else {
			throw new YailRuntimeError("SetValue: component not found", this.TAG);
		}
	}

	@Deprecated
	@SimpleFunction(description = "Set a property of the specified component. 1.if property is same to one of the in-build perperty, it will set this perperty. 2.if property is 'index', it will set the order of the component.3. if any other string, it will attach a custom property on this component")
	public void Set(Component component, @Options(Property.class) String property, Object value) {
		SetValue(component, property, value);
	}

	@SimpleFunction(description = "pls refer to Set function")
	public Object GetValue(Component component, @Options(Property.class) String property) {
		if (this.PARENT.containsKey(component)) {
			if (property.toLowerCase().equals("index")) {
				return Integer.valueOf(GetIndex((AndroidViewComponent) component));
			}
			Method[] arrayOfMethod = component.getClass().getMethods();
			Object[] arrayOfObject = YailList.makeEmptyList().toArray();
			Method localMethod = getMethod(arrayOfMethod, property, arrayOfObject.length);
			if (localMethod == null) {
				HashMap prop = (HashMap) this.DICT.get(component);
				if (prop == null) {
					return "";
				}
				Object value = prop.get(property);
				if (value == null) {
					return "";
				}
				return value;
			}
			return Invoke(component, property, YailList.makeEmptyList());
		}
		throw new YailRuntimeError("GetValue: component not found", this.TAG);
	}

	@Deprecated
	@SimpleFunction(description = "pls refer to Set function")
	public Object Get(Component component, @Options(Property.class) String property) {
		return GetValue(component, property);
	}

	private void SetIndex(AndroidViewComponent component, int index) {
		View view = component.getView();
		ViewGroup parent = (ViewGroup) view.getParent();
		if (index < 1)
			index = 1;
		else if (index > parent.getChildCount()) {
			index = parent.getChildCount();
		}
		index--;

		parent.removeView(view);
		parent.addView(view, index);
	}

	private int GetIndex(AndroidViewComponent component) {
		View view = component.getView();
		ViewGroup parent = (ViewGroup) view.getParent();
		return parent.indexOfChild(view) + 1;
	}

	private Object Invoke(Component paramComponent, String paramString, YailList paramYailList) {
		if (!isEmptyOrNull(paramComponent)) {
			Object localObject1 = null;
			Method[] arrayOfMethod = paramComponent.getClass().getMethods();
			try {
				Object[] arrayOfObject = paramYailList.toArray();
				Method localMethod = getMethod(arrayOfMethod, paramString, arrayOfObject.length);
				Class[] arrayOfClass = localMethod.getParameterTypes();
				ArrayList localArrayList = new ArrayList();
				for (int i = 0; i < arrayOfClass.length; i++) {
					if ("int".equals(arrayOfClass[i].getName()))
						localArrayList.add(Integer.valueOf(Integer.parseInt(arrayOfObject[i].toString())));
					else if ("float".equals(arrayOfClass[i].getName()))
						localArrayList.add(Float.valueOf(Float.parseFloat(arrayOfObject[i].toString())));
					else if ("double".equals(arrayOfClass[i].getName()))
						localArrayList.add(Double.valueOf(Double.parseDouble(arrayOfObject[i].toString())));
					else if ("java.lang.String".equals(arrayOfClass[i].getName()))
						localArrayList.add(arrayOfObject[i].toString());
					else if ("boolean".equals(arrayOfClass[i].getName()))
						localArrayList.add(Boolean.valueOf(Boolean.parseBoolean(arrayOfObject[i].toString())));
					else {
						localArrayList.add(arrayOfObject[i]);
					}
				}
				localObject1 = localMethod.invoke(paramComponent, localArrayList.toArray());
			} catch (Exception localException) {
			} finally {
				if (isEmptyOrNull(localObject1)) {
					return "";
				}
			}
			return localObject1;
		}
		throw new YailRuntimeError("Component cannot be null.", this.TAG);
	}

	private boolean isEmptyOrNull(Object paramObject) {
		if ((paramObject instanceof String)) {
			String str = paramObject.toString();
			str = str.replace(" ", "");
			return str.isEmpty();
		}
		return paramObject == null;
	}

	@Deprecated
	@SimpleFunction(description = "CreateFrom")
	public void CreateFrom(String template, Object in, YailList values) {
	}

	private void initParentChildrenMap(Object component) {
		if ((component instanceof ComponentContainer))
			try {
				ArrayList temp = new ArrayList();

				if ((component instanceof Canvas)) {
					Canvas canvas = (Canvas) component;
					List sprites = canvas.getChildren();

					for (Object sprite : sprites) {
						temp.add(sprite);
					}
				} else if ((component instanceof Map)) {
					Log.i(this.TAG, "found map, ignore it");
				} else {
					temp = (ArrayList) ((ComponentContainer) component).getClass()
							.getMethod("getChildren", new Class[0]).invoke(component, new Object[0]);
				}

				this.CHILDREN.put(component, temp);

				for (int i = 0; i < temp.size(); i++) {
					Object item = temp.get(i);
					if (!this.PARENT.containsKey(item)) {
						this.PARENT.put(item, component);
					}
					if ((item instanceof ComponentContainer))
						initParentChildrenMap(item);
				}
			} catch (Exception e) {
				throw new YailRuntimeError(e.getMessage(), this.TAG);
			}
	}

	@SimpleFunction(description = "CreateWith")
	public Object CreateWith(String template, Object in, YailList values) {
		template = template.replaceFirst("components", "Components");
		template = replaceWithList(template, values);
		List allComponets = new ArrayList();
		Object result = null;
		try {
			JSONObject temp = new JSONObject(template);
			JSONArray arr = temp.getJSONArray("$Components");
			for (int i = 0; i < arr.length(); i++) {
				JSONObject object = arr.getJSONObject(i);
				allComponets.add(in);
				allComponets.add(object);
			}
			String[] keysArr = { "$Name", "$Type", "$Version", "$Components", "Uuid", "$blocks" };
			List keywords = Arrays.asList(keysArr);
			while (allComponets.size() > 0) {
				JSONObject properties = (JSONObject) allComponets.remove(1);
				Object container = allComponets.remove(0);

				String type = properties.getString("$Type");

				Object component = Create(type, container);

				Iterator iterator = properties.keys();
				while (iterator.hasNext()) {
					String key = (String) iterator.next();
					if (!keywords.contains(key)) {
						Object value = properties.get(key);
						if (key.indexOf("Color") > -1) {
							value = Integer.valueOf(parseColor((String) value));
						}
						SetValue((Component) component, key, value);
					}
				}
				if (result == null) {
					result = component;
				}
				if (properties.has("$Components")) {
					JSONArray tempArray = properties.getJSONArray("$Components");
					for (int i = 0; i < tempArray.length(); i++) {
						allComponets.add(component);
						allComponets.add(tempArray.getJSONObject(i));
					}
				}
			}
		} catch (JSONException e) {
			throw new YailRuntimeError(e.getMessage(), this.TAG);
		}
		return result;
	}

	private int parseColor(String str) {
		if (str.startsWith("&H")) {
			str = str.substring(2);
			return Color.parseColor("#" + str);
		}
		if (str.startsWith("#")) {
			return Color.parseColor(str);
		}
		Long b = Long.valueOf(Long.parseLong(str));
		if (b.longValue() > 0L) {
			String hexString = Long.toHexString(b.longValue());

			if (hexString.length() == 7)
				hexString = "0" + hexString;
			else if (hexString.length() == 6) {
				hexString = "00" + hexString;
			}
			return Color.parseColor("#" + hexString);
		}
		return Integer.parseInt(str);
	}

	private String replaceWithList(String str, YailList params) {
		for (int i = 1; i <= params.size(); i++) {
			str = str.replaceAll("\\\\" + i, params.get(i).toString()).replaceAll("\\{" + i + "\\}",
					params.get(i).toString());
		}
		return str;
	}

	@SimpleFunction(description = "OnClick")
	public void OnClick(Component component, String listener) {
		ArrayList<Component> list = new ArrayList<>();
		list.add(component);
		YailList arguments = YailList.makeList(list);

		final ProcedureN fun = lookupProcedure(listener);
		if (fun == null) {
			throw new YailRuntimeError("Unable to locate procedure '" + listener + "'", this.TAG);
		}
		if (fun.minArgs() != 1) {
			throw new YailRuntimeError("Procedure '" + listener + "' need 1 args, but got " + arguments.size(),
					this.TAG);
		}

		View view = ((AndroidViewComponent) component).getView();
		view.setOnClickListener(new View.OnClickListener() {
			public void onClick(View arg0) {
				CompCreator.this.doCall(fun, arguments);
			}
		});
	}

	@SimpleFunction(description = "OffClick")
	public void OffClick(Component component) {
		View view = ((AndroidViewComponent) component).getView();
		view.setOnClickListener(null);
	}

	private Object doCall(ProcedureN func, YailList arguments) {
		try {
			if ((arguments == null) || (func.numArgs() == 0)) {
				return func.apply0();
			}
			Object[] argarray = new Object[arguments.size()];
			int i = 0;
			Iterator it = arguments.iterator();
			it.next();
			while (it.hasNext()) {
				argarray[(i++)] = it.next();
			}
			return func.applyN(argarray);
		} catch (Throwable throwable) {
			throw new YailRuntimeError(throwable.getMessage(), this.TAG);
		}
	}

	private ProcedureN lookupProcedureInRepl(String procedureName) {
		Scheme lang = Scheme.getInstance();
		try {
			Object result = lang
					.eval("(begin (require <com.google.youngandroid.runtime>)(get-var p$" + procedureName + "))");

			if ((result instanceof ProcedureN))
				return (ProcedureN) result;
		} catch (Throwable throwable) {
			throwable.printStackTrace();
		}
		return null;
	}

	private ProcedureN lookupProcedureInForm(String procedureName) {
		try {
			Field globalVarEnvironment = this.form.getClass().getField("global$Mnvars$Mnto$Mncreate");
			LList vars = (LList) globalVarEnvironment.get(this.form);

			Symbol procSym = new SimpleSymbol("p$" + procedureName);
			Object result = null;
			for (Iterator localIterator = vars.iterator(); localIterator.hasNext();) {
				Object pair = localIterator.next();
				if (!LList.Empty.equals(pair)) {
					LList asPair = (LList) pair;
					if (((Symbol) asPair.get(0)).getName().equals(procSym.getName())) {
						result = asPair.get(1);
						break;
					}
				}
			}
			if ((result instanceof ProcedureN))
				return (ProcedureN) ((ProcedureN) result).apply0();
		} catch (Throwable throwable) {
			throwable.printStackTrace();
		}
		return null;
	}

	private ProcedureN lookupProcedure(String procedureName) {
		if ((this.form instanceof ReplForm)) {
			return lookupProcedureInRepl(procedureName);
		}
		return lookupProcedureInForm(procedureName);
	}
}
